import json
import re
import pandas as pd
import plotly as pppppp
from plotly import version
import plotly.express as px
import nltk
from nltk.parse.corenlp import CoreNLPDependencyParser
from nltk.corpus import wordnet as wn
from nltk.corpus import opinion_lexicon
from nltk.sentiment import SentimentIntensityAnalyzer
from nltk.sentiment.vader import VaderConstants
from nltk.corpus import sentiwordnet as swn
from nltk.parse.corenlp import CoreNLPDependencyParser
pppppp.__version__
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
nltk.download('wordnet')
nltk.download('opinion_lexicon')
nltk.download('vader_lexicon')
nltk.download('sentiwordnet')
nltk.download('omw-1.4')
Implementamos una función que nos devuelva una lista de una reviews dado una conujunto de datos.
def getReviews(data):
reviewsT = []
for review in data:
reviewsT.append(review.get('reviewText'))
return reviewsT
Partiendo del código de ejemplo proporcionado en el enunciado de la practica.
with open('yelp_dataset/yelp_hotels.json', encoding='utf-8') as f:
reviews = json.load(f)
f.close()
numReviews = len(reviews)
print(numReviews, 'reviews loaded')
print(reviews[0])
print(reviews[0].get('reviewerID'))
Escribimos una función para leer linea a linea los datasets, abrimos el archivo correspondiente y lo vamos recorriendo línea a linea, parseando el texto con saltos de línea.
def loadReview_byLine(dir_):
data = []
file = open(dir_, encoding='utf-8')
for line in file:
if line != '[\n' and line != ']':
line = line.rstrip(',\n')
data.append(json.loads(line))
file.close()
return data
data_hotel = loadReview_byLine('yelp_dataset/yelp_hotels.json')
print(data_hotel[0])
Cargamos los datasets linea por linea:
data_spas = loadReview_byLine('yelp_dataset/yelp_beauty_spas.json')
print(data_spas[0])
data_restaurants = loadReview_byLine('yelp_dataset/yelp_restaurants.json')
print(data_restaurants[0])
Definimos una función para cargar los aspectos de un fichero de texto y almacenarlos en estructura de diccionario. El vocabulario devuelto tiene estructura de diccionario: {"<"Aspect">": set("<"term1">", ..., "<"termN">")}
def dic_aspect(file):
aspects = {}
f = open(file, "r")
for l in f:
tokens = l.rstrip('\n').split(',')
if tokens[0] not in aspects:
aspects[tokens[0]] = set()
aspects[tokens[0]].add(tokens[1])
f.close()
return aspects
Cargamos los vocabularios de los ficheros de aspectos proporcionados de los dominios de hotels, además de los dominios de books,cds, digitalmusic,etc.
aspects_hotel = dic_aspect("aspects/aspects_hotels.csv")
len(aspects_hotel)
Obtenemos los 31 aspects, que son:
pd.DataFrame(aspects_hotel.keys(), columns=['Aspects'])
Imprimimos el vocabulario completo de los aspect terms para el ámbito de los hoteles, mediante una función auxiliar:
def print_aspects(aspects):
data_tuples = list(zip(aspects.keys(),aspects.values()))
return pd.DataFrame(data_tuples, columns=['Aspect','Term'])
print_aspects(aspects_hotel)
Dada una review, para determinar aspectos, hacemos mediante "exact matching" la identificación de nombres dentro de la review. Si alguna de esas palabras aparece en el listado de un aspecto de nuestro vocabulario, identificamos como que hablamos de ese aspecto en la review. Para ello primero definimos la función:
import re
def get_review_apects(review, vocabulary):
'''
Args:
review:text
vocabulary: vocabulary of aspects
Returns:
pd dataframe with pairs word-aspect found in the review
'''
aspects = []
terminos = []
#Limpiando signos de puntuación
text = re.sub('\ |\?|\.|\!|\/|\;|\:|\,', ' ', review)
unique_words = set(text.lower().split(' '))
#print('unique_words', unique_words)
for a,terms in vocabulary.items():
#first list compr. to accelerate the function
found_words = [w for w in unique_words if w in terms]
for w in found_words:
aspects.append(a)
terminos.append(w)
rev_aspects = pd.DataFrame( {'Aspect': aspects, 'Term': terminos})
return rev_aspects
Para la primera review del dataset e imprimimos sus aspectos encontrados junto con el término que hizo que identificáramos dicho aspecto en la review('exact matching').
data_hotel[0].get('reviewText')
aspectsH = get_review_apects(data_hotel[0].get('reviewText'), aspects_hotel)
aspectsH
Dado que para un término puede haber más de un synset de acuerdo a diferentes definiciones del término, vamos a coger únicamente la primera acepción, por ser la más común. Esto es debido a que experimentalmente introduce muchos términos de media que no guardan contexto real con las opiniones, por lo que nos incluye 'basura' al modelo.
Por ejemplo en el caso de "amenity" nos introduce "sweetness", debido muy probablemente a que algun hiperónico/hipónimo de alguna de las acepciones que no es la primera.
Aún así, hemos decidido mantener únicamente la primera, dado que el alcance de nuestro dataset de hoteles es de fin generalista. Es decir, que al no requerir se muy tecnico dado el contexto, lo más probable es que en las reviews se utilicen palabras del lenguaje común, y no las acepciones más técnicas o con fin literario alto para las mismas.
Para extender la lista de términos, cogemos y para cada aspect extendemos su lista de términos tal que:
1. Extendemos con los sinónimos
2. Extendmos con los hipónimos(por ser realiaciones más concretas)
3. Extendemos por hiperónimos(al poder existir alguna palabra más generalista que el propio aspect )
De forma promedia, esperamos que por lo general la extensión con terminos sea más beneficiosa que perjudicial para nuestros aspectos.
Como trabajo futuro, una opción para mejorar esta función sería, que en vez de hacer una función que mire en aspectos de forma "estática", modificar la función para que además recibierá el contexto de la frase como un parámetro adicional, de tal forma que fueramos capaz de extraer el contexto donde se ubica la palabra, tomando del mismo la acepción y por lo tanto el aspecto más adecuado para la frase en cuestión.
import copy
def extend_vocabulary_wordnet(vocabulary):
'''
Args:
Aspects vocabulary.
Returns:
Entended vocabulary
'''
extended_vocabulary = copy.deepcopy(vocabulary)
for aspect in list(vocabulary.keys()):
#print('antes',extended_vocabulary[aspect] )
synsets = wn.synsets(aspect)
if any(synsets): #cambio
synset = synsets[0] #acepcione primera
extended_vocabulary[aspect].update(synset.lemma_names())#sinonimos
for h in synset.hypernyms():#hiperonimos
extended_vocabulary[aspect].update(h.lemma_names())
#extended_vocabulary[aspect].update(synset.hypernyms())#hiperonimos
for h in synset.hyponyms():#hiponimos(mas especificos)
extended_vocabulary[aspect].update(h.lemma_names())
#print('despues',extended_vocabulary[aspect] )
return extended_vocabulary
aspects_hotel_extended_wordnet = extend_vocabulary_wordnet(aspects_hotel)
print_aspects(aspects_hotel_extended_wordnet)
Observamos que ganamos muchos términos que nos serán útiles en el futuro.
Como problema vemos que nos intruduce guiones(altos y bajos) en algunos términos, como si fuera una palabra. Sin embargo, dado que es probable que algun usuario lo llegue a utilizar, y no nos perjudica el modelo, para la propuesta de "exact matching" para identificar aspectos de términos, lo dejamos así.
Vamos a ver que obtenemos para el la review 0 de los hoteles del ejercicio 2.1 para nuestro vocabulario extendido:
aspectsH_2 = get_review_apects(data_hotel[0].get('reviewText'), aspects_hotel_extended_wordnet)
aspectsH_2
Vemos como el término hotel antes no nos lo identificaba, y además ganamos un nuevo aspect en la opinion, el de events, ya que la palabra happening nos dicta que podría tratarse de un evento.
De igual modo que en el apartado 2.1, cargamos el vocabulario de aspectos para otros dominios de Yelp, que no son el de los hoteles:
Por ejemplo para el vocabulario del dominio de los libros, del archivo 'aspects/aspects_books.csv' tenemos 16 aspectos:
def count_aspects(aspects):
count = 0
for aspect in aspects:
count += len(aspects.get(aspect))
return count
Imprimimos su vocabulario de aspectos y el total de terminos asociados a estos aspectos.
aspects_books = dic_aspect("aspects/aspects_books.csv")
print("count_aspects: ", len(aspects_books) ," count_terms: " , count_aspects(aspects_books))
print_aspects(aspects_books)
Ahora lo ampliamos con WordNet
aspects_books2 = extend_vocabulary_wordnet(aspects_books)
print_aspects(aspects_books2)
print("count_aspects: " , (len(aspects_books2)) , " count_terms: " , count_aspects(aspects_books2))
diff = count_aspects(aspects_books2) - count_aspects(aspects_books)
print("\tdiff: " , diff)
Vemos como ahora hemos aumentado considerablemente el número de terminos, 232 para ser exactos en este ejemplo ya que pasamos de los 147 terminso originales a los 379 del aspecto extendido.
De igual modo, cargamos los vocabularios de el resto de archivos de la carpeta aspects,aunque por longitud de el notebook no los imprimos todos, por ser similares al vocabulario de los libros. Para cada uno de ellos podemos ver el número de aspectos:
aspects_cds = dic_aspect("aspects/aspects_cds.csv")
aspects_cds2 = extend_vocabulary_wordnet(aspects_cds)
print("Original\tcount_aspects: " , (len(aspects_cds)) , " count_terms: " , count_aspects(aspects_cds))
print("Extended\tcount_aspects: " , (len(aspects_cds2)) , " count_terms: " , count_aspects(aspects_cds2))
diff = count_aspects(aspects_cds2) - count_aspects(aspects_cds)
print("\tdiff: " , diff)
aspects_digitalmusic = dic_aspect("aspects/aspects_digitalmusic.csv")
aspects_digitalmusic2 = extend_vocabulary_wordnet(aspects_digitalmusic)
print("Original\tcount_aspects: " , (len(aspects_digitalmusic)) , " count_terms: " , count_aspects(aspects_digitalmusic))
print("Extended\tcount_aspects: " , (len(aspects_digitalmusic2)) , " count_terms: " , count_aspects(aspects_digitalmusic2))
diff = count_aspects(aspects_digitalmusic2) - count_aspects(aspects_digitalmusic)
print("\tdiff: " , diff)
aspects_movies = dic_aspect("aspects/aspects_movies.csv")
aspects_movies2 = extend_vocabulary_wordnet(aspects_movies)
print("Original\tcount_aspects: " , (len(aspects_movies)) , " count_terms: " , count_aspects(aspects_movies))
print("Extended\tcount_aspects: " , (len(aspects_movies2)) , " count_terms: " , count_aspects(aspects_movies2))
diff = count_aspects(aspects_movies2) - count_aspects(aspects_movies)
print("\tdiff: " , diff)
aspects_phones = dic_aspect("aspects/aspects_phones.csv")
aspects_phones2 = extend_vocabulary_wordnet(aspects_phones)
print("Original\tcount_aspects: " , (len(aspects_phones)) , " count_terms: " , count_aspects(aspects_phones))
print("Extended\tcount_aspects: " , (len(aspects_phones2)) , " count_terms: " , count_aspects(aspects_phones2))
diff = count_aspects(aspects_phones2) - count_aspects(aspects_phones)
print("\tdiff: " , diff)
aspects_restaurants = dic_aspect("aspects/aspects_restaurants.csv")
aspects_restaurants2 = extend_vocabulary_wordnet(aspects_restaurants)
print("Original\tcount_aspects: " , (len(aspects_restaurants)) , " count_terms: " , count_aspects(aspects_restaurants))
print("Extended\tcount_aspects: " , (len(aspects_restaurants2)) , " count_terms: " , count_aspects(aspects_restaurants2))
diff = count_aspects(aspects_restaurants2) - count_aspects(aspects_restaurants)
print("\tdiff: " , diff)
aspects_spas = dic_aspect("aspects/aspects_spas.csv")
aspects_spas2 = extend_vocabulary_wordnet(aspects_spas)
print("Original\tcount_aspects: " , (len(aspects_spas)) , " count_terms: " , count_aspects(aspects_spas))
print("Extended\tcount_aspects: " , (len(aspects_spas2)) , " count_terms: " , count_aspects(aspects_spas2))
diff = count_aspects(aspects_spas2) - count_aspects(aspects_spas)
print("\tdiff: " , diff)
aspects_videogames = dic_aspect("aspects/aspects_videogames.csv")
aspects_videogames2 = extend_vocabulary_wordnet(aspects_videogames)
print("Original\tcount_aspects: " , (len(aspects_videogames)) , " count_terms: " , count_aspects(aspects_videogames))
print("Extended\tcount_aspects: " , (len(aspects_videogames2)) , " count_terms: " , count_aspects(aspects_videogames2))
diff = count_aspects(aspects_spas2) - count_aspects(aspects_videogames)
print("\tdiff: " , diff)
Ahora imprimimos los aspects encontrados en alguna review de ejemplo de igual modo que hacíamos en el 2.1 para los datasets de reviews spas y restaurants junto con sus correspondientes vocabularios.
Mostramos un ejemplo con la primera review de Spas:
data_spas[0].get('reviewText')
aspectsS = get_review_apects(data_spas[0].get('reviewText'), aspects_spas)
aspectsS
Mostramos otro ejemplo, esta vez con la primera reseña del dataset de Restaurantes:
data_restaurants[0].get('reviewText')
aspectsR = get_review_apects(data_restaurants[0].get('reviewText'), aspects_restaurants)
aspectsR
Cargamos en primer lugar el lexicon de Liu, que incluye palabras con polaridad positiva y negativa.
negativeWords = opinion_lexicon.negative()
positiveWords = opinion_lexicon.positive()
print(negativeWords)
print(len(negativeWords))
print(positiveWords)
print(len(positiveWords))
Creamos un primera función basica que calcule la polaridad entre [-1,1] dependiendo de si la polaridad es positiva o negativa
def polarity_word(word):
if word in positiveWords:
return 1
elif word in negativeWords:
return -1
return 0
La testeamos, vemos como una palabra como very la considera neutra.
print('great:', polarity_word('great'))
print('awesome:', polarity_word('awesome'))
print('ugly:', polarity_word('ugly'))
print('very:', polarity_word('very'))
#TODO, quitar esto si luego no lo usamos.
Cargamos el lexicon "Vader Opinion lexicon",que incluye emojis,abreviaciones y acrónimos.
f = nltk.data.load("vader_lexicon/vader_lexicon.txt")
lexicon = {}
for line in f.split("\n"):
(word, polarity) = line.strip().split("\t")[0:2]
lexicon[word] = float(polarity)
lexicon.get("")
Ampliamos la función de polaridad básica para que utilice el léxico de Vader, como este léxico devuelve polaridades en un rango diferente de valores, lo normalizamos para que devuelva valores entre [-1,1].
def polarity_word_Vader(word):
if word in positiveWords:
return 1
elif word in negativeWords:
return -1
elif word in lexicon:
if lexicon[word] >= 0:
return 1
else:
return -1
return 0
text = data_hotel[0].get('reviewText')
for word in re.findall(r"[\w']+", text):
polarity = polarity_word_Vader(word)
if polarity != 0:
print('term: ', word , '\tpolaridad: ', polarity)
Combinando ambos lexicon's definimos una función para obtener la polaridad de un texto.
def count_polarity_words(text):
count = 0
words = 0
for word in re.findall(r"[\w']+", text):
if word in positiveWords:
count += 1
words += 1
elif word in negativeWords:
count -= 1
words += 1
elif word in lexicon:
count += lexicon[word]
words += 1
return count/words
Mostramos un ejemplo, tomando de nuevo la primera review de Hoteles:
data_hotel[0].get('reviewText')
count_polarity_words(data_hotel[0].get('reviewText'))
Definimos una función para que nos de el porcentaje de polaridad por review, dado un conjunto de reviews. Es decir, si una review está cargada de sentimientos y juicios de valor(buenos/malos) sobre diferentes aspectos. Cuanto mayor sea el valor de la polaridad más positiva será, es decir una polaridad negativa indicará una review crítica, una con una polaridad cercana a 0 indiracará una review neutra por lo general y una polaridad cercana a uno indicará una valoraciónbmuy positiva. Algo así como la review de una película de un crítico de cine y el comentario de un amigo que la ha visto.
En este ultimo ejemplo vemos que la polaridad es positiva, ya que tenemos fragmentos como "Great hotel" ,"awesome", "Great boutique rooms", "Awesome pool" "GREAT rooftop patio bar" "A great place to stay" que hacen que la opinión tomé una polaridad positiva. Pero no tenemos un 1 absoluto ya que tenemos fragmentos como "a very very busy lobby with Gallo Blanco attached" las cuales penalizan la opinión.
Pasamos a hacer una función auxiliar que evalue varias de las opiniones:
def count_polarity_data(data_list):
count = 0
for data in data_list:
pol = count_polarity_words(data.get('reviewText'))
count += pol
print("reviewerID:", data.get('reviewerID'), "got a polarity of ", pol)
print("Avg polarity: " , count/len(data_list))
Hacemos un ejemplo en el cual tomando las 10 primeras opiniones del dataset de Hoteles muestre las polaridades de las diez primeras reseñas y saque una media de las mismas. En este caso lo que obtenemos es que las diez primeras oiniones son en su mayoria positivas.
count_polarity_data(data_hotel[:10])
Aún así estos listados para calcular la polaridad no son únicos, hay más metodos que podríamos explorar tales como:
#https://stackoverflow.com/questions/38263039/sentiwordnet-scoring-with-python
print(list(swn.senti_synsets('happy')))
polarity = swn.senti_synset('happy.a.01')
print('pos', polarity.pos_score(), 'neg', polarity.neg_score())
Creamos una nueva función auxiliar basandonos en la libreria sentiwordnet y ver si altera nuestros resultados previos:
def count_polaritySentiment_words(text):
count = 0
words = 0
for word in re.findall(r"[\w']+", text):
sentiment = list(swn.senti_synsets(word))
if(any(sentiment)):
count += sentiment[0].pos_score()
count -= sentiment[0].neg_score()
words += 1
return count/words
data_hotel[0].get('reviewText')
count_polaritySentiment_words(data_hotel[0].get('reviewText'))
Vemos que en esta ocasión la polaridad de la sentencia es mucho más cercana a 0, lo cual denota cierta neutralidad, esto es principalmente debido a que esta libreria toma en cuenta más palabras que nuestra implementación casera, por lo que muchas palabras(neutras en su mayoria) aproximan la polaridad de la base a un valor más neutral.
Por otro lado nuestro metodo asignaba valores de +1/-1 dependiendo de si la palabra, mientras que aquí Sentiment_words no hace una valoración tan extrema.
def count_polaritySentiment_words_data(data_list):
count = 0
for data in data_list:
pol = count_polaritySentiment_words(data.get('reviewText'))
count += pol
print("reviewerID:", data.get('reviewerID'), "got a polarity of ", pol)
print("Avg polarity: " , count/len(data_list))
count_polaritySentiment_words_data(data_hotel[:10])
Tal como sospechabamos, este nuevo metodo se aproxima más a la neutralidad, por lo que no nos aporta infromación tan interesante como la primera implementación, por lo que tal vez Liu es más adecuado para este enfoque.
Probamos esta nueva versión para el calculo de polaridad siguiendo el apendice G. Como es habitual primero vamos a hacer una prueba con la primera review del datset de Hoteles.
constants = VaderConstants()
print(constants.BOOSTER_DICT)
analyzer = SentimentIntensityAnalyzer()
data_hotel[0].get('reviewText')
analyzer.polarity_scores(data_hotel[0].get('reviewText'))
En este caso vemso que el metodo es capaz de analizar toda una sentencia y dar una valoración final dividida en varios puntos:
De estos valores el que más nos interesa es el de compound que sintetixa los otros tres, lo sustituiremos por nuestra función de polaridad en el 4.3.
Definimos una función para extraer hacer POS tagging a un text(review), es decir, identificamos los adjetivos.
def pos_tagging(text):
sentences = nltk.sent_tokenize(text)
sentences = [nltk.word_tokenize(s) for s in sentences]
sentences = [nltk.pos_tag(s) for s in sentences]
return sentences
Vamos a verlo con el ejemplo usado anteriormente:
postagged_sentences = pos_tagging(reviews[0].get("reviewText").lower())
print(reviews[0].get("reviewText"))
print(postagged_sentences[0])
print(postagged_sentences[1])
Vamos a definir una función para que nos devuelva los adjetivos dado un texto(en nuestro paso lo usaremos para las frases):
def get_adjetivos_sentence(sentence):
s = nltk.word_tokenize(sentence)
adjetivos = [(w,tipo) for w,tipo in nltk.pos_tag(s) if tipo == 'JJ']
return adjetivos
test_sentence = 'Great hotel in Central Phoenix for a stay-cation, but not necessarily a place to stay out of town and without a car.'
get_adjetivos_sentence(test_sentence)
Consideramos la gramática basica: "adjetivo" + "nombre común/propio", por proporcionarnos resultados muy buenos experimentales a a la hora de detectar descripciones de aspectos.
Definimos la siguiente función que dado el texto de una review y un vocabulario de aspectos, nos da un resumen sobre la polaridad de los aspectos descritos en ella(ver ejemplo debajo). Para ello consideramos la polaridad de los adjetivos que van delante de términos, que pertenecen a nuestro vocabulario de aspectos. Todo ello junto al uso del POS Tagging + Syntactic analysis de NLTK.
def get_review_resume(review_text, aspect_vocabulary):
''' Devuelve el resumen de una review, como el ejemplo inicial de la práctica.(Solo para los
aspectos disponibles en el vocabulario pasado)
Args:
review_text: texto de la review
aspect_vocabulary: vocabulario de aspectos
Returns:
rev_resume: df with aspect valoration found
'''
#Definimos la gramática usada para detectar adjetivos de nombres.
grammar = r"""
JJNN: {<JJ>*<NN>+} # chunk adjective and sequences noun
{<JJ>*<NNP>+} # chunk sequences of proper nouns
"""
cp = nltk.RegexpParser(grammar)
#Obtenemos los aspectos presentes en la review
aspectsH = get_review_apects(review_text, aspect_vocabulary)
rows = []
row = []
for pos in pos_tagging(review_text.lower()):#USO DE POS TAGGING
chunk_parse = cp.parse(pos)
for child in chunk_parse:
if isinstance(child, nltk.Tree):
if child.label() == 'JJNN':#JNN:gramática
word = ""
adjective = ""
polarity = 0
#aspects = ""
aspects = []
#print('child: ', child)
#Según la casuística de nuestra gamática, exploramos los posibles casos
for i in range(len(child)):
if child[i][1] == 'JJ':#adjetivo
#calculo la polaridad del adjetivo por si esta asociado a un term conocido
polarity += polarity_word(child[i][0])
adjective += child[i][0] + " "
if child[i][1] == "NN" or child[i][1] == 'NNP':#Nombre
#miramos si la palabra es un aspect term + calculamos
word += child[i][0] + " "
aux = aspectsH.loc[aspectsH['Term'] == child[i][0]]['Aspect']
if(aux.empty == False):#tenemos aspect con esa palabra
#aspects += aux.values[0] + " "
aspects.append((aux.values[0] + " "))
#Polarity Aspect Adjective Word
if(any(aspects)):
#Consideramos el aspecto de la ultima palabra:p.e: rooftop patio bar -->bar
row = [polarity, aspects[-1], adjective, word]
else:
row = [polarity, '', adjective, word]
rows.append(row)
rev_resume = pd.DataFrame(rows, columns =['Polarity', 'Aspect', 'Adjective', 'Word'])
#Devolvemos SOLO las que contengan un aspect y un adjetivo de nuestro vocabulario
df = rev_resume.loc[(rev_resume['Aspect'] != '') & (rev_resume['Adjective'] != '') ].reset_index(drop=True)
return df
Probamos nuestra función con la review de ejemplo utilizada en todo el notebook:
data_hotel[0].get('reviewText')
get_review_resume(data_hotel[0].get('reviewText'), aspects_hotel)
Probamos ahora con el vocabulario extendido por wordnet del apartado 2.2 :
get_review_resume(data_hotel[0].get('reviewText'), aspects_hotel_extended_wordnet)
Vemos como hemos ganado una entrada más en la tabla, al disponer de un vocabulario ampliado, lo cual nos interesa para extraer luego información sobre el hotel con todas las reviews sobre él.
def get_review_resumeSIA(review_text, aspect_vocabulary):
''' Devuelve el resumen de una review, como el ejemplo inicial de la práctica.(Solo para los
aspectos disponibles en el vocabulario pasado)
Args:
review_text: texto de la review
aspect_vocabulary: vocabulario de aspectos
Returns:
rev_resume: df with aspect valoration found
'''
#Definimos la gramática usada para detectar adjetivos de nombres.
grammar = r"""
JJNN: {<JJ>*<NN>+} # chunk adjective and sequences noun
{<JJ>*<NNP>+} # chunk sequences of proper nouns
"""
cp = nltk.RegexpParser(grammar)
#Obtenemos los aspectos presentes en la review
aspectsH = get_review_apects(review_text, aspect_vocabulary)
rows = []
row = []
for pos in pos_tagging(review_text.lower()):#USO DE POS TAGGING
chunk_parse = cp.parse(pos)
for child in chunk_parse:
if isinstance(child, nltk.Tree):
if child.label() == 'JJNN':#JNN:gramática
word = ""
adjective = ""
polarity = 0
#aspects = ""
aspects = []
#print('child: ', child)
#Según la casuística de nuestra gamática, exploramos los posibles casos
for i in range(len(child)):
if child[i][1] == 'JJ':#adjetivo
#calculo la polaridad del adjetivo por si esta asociado a un term conocido
polarity += analyzer.polarity_scores(child[i][0])['compound']
#ANTERIORMENTEpolarity_word(child[i][0])
adjective += child[i][0] + " "
if child[i][1] == "NN" or child[i][1] == 'NNP':#Nombre
#miramos si la palabra es un aspect term + calculamos
word += child[i][0] + " "
aux = aspectsH.loc[aspectsH['Term'] == child[i][0]]['Aspect']
if(aux.empty == False):#tenemos aspect con esa palabra
#aspects += aux.values[0] + " "
aspects.append((aux.values[0] + " "))
#Polarity Aspect Adjective Word
if(any(aspects)):
#Consideramos el aspecto de la ultima palabra:p.e: rooftop patio bar -->bar
row = [polarity, aspects[-1], adjective, word]
else:
row = [polarity, '', adjective, word]
rows.append(row)
rev_resume = pd.DataFrame(rows, columns =['Polarity', 'Aspect', 'Adjective', 'Word'])
#Devolvemos SOLO las que contengan un aspect y un adjetivo de nuestro vocabulario
df = rev_resume.loc[(rev_resume['Aspect'] != '') & (rev_resume['Adjective'] != '') ].reset_index(drop=True)
return df
data_hotel[0].get('reviewText')
get_review_resumeSIA(data_hotel[0].get('reviewText'), aspects_hotel)
get_review_resumeSIA(data_hotel[0].get('reviewText'), aspects_hotel_extended_wordnet)
Este método lo único que cambia es que estamos usando un calculo de la polaridad más suave, por lo que las polaridades de las sentencias son más "grises" y no tan extremas como en el primer método, de normal las opiniones no son tan drasticas de blanco/negro, por lo que toman un mejor valor en cuanto a la realidad frente a nuestro metodo "casero".
Esta visualización es la que hemos hecho ya previamente en el apartado 4.1 para ver la salida de nuestra implementación. Repetimos la visualización por organización con el vocabulario extendido por organización del notebook(para ver mas detalles ver apartado 4.1)
RECUERDA: nuestra polaridad la damos en el intervalo [-1,1]
review = data_hotel[0]
review_text = review.get('reviewText')
get_review_resume(review_text, aspects_hotel_extended_wordnet)
Ahora para cada hotel, con el conjunto de todas las reviews escritas sobre el, damos la polaridad de cada aspecto. Para ello definimos la siguiente función, que dado un hotel nos da el resumen que queremos.
Además para cada item(hotel), buscamos el top N de términos(con aspecto relacionado), que se mencionan sobre él. Esto nos es útil para ver información que es lo que más le llama la atención a la gente de un sitio concreto. En caso de empate se eligen al azar.
NOTA: (Para los gráficos se ha usado la version '4.14.3') de plotly.
from collections import Counter
def aux_fun_add_polarity_to_aspect(aspect,polarity,aspects_polarity ):
if(not aspect) in aspects_polarity:
aspects_polarity[aspect] = []
aspects_polarity[aspect].append(polarity)
def aspect_opinion_by_item(item_id, reviews,aspect_vocabulary, debug = False, sort_by_ascending_plarity = False, n_most_common_words=5):
'''
Args:
item_id: hotel id
reviews: reviews (file loaded into the notebook)
aspect_vocabulary: vocabulario de aspectos
n_most_common_words: get the top N commmon terms(with an ascpect identified) metieoned about the item
Returns:
resume(pandas dataframe) :Resume for a particular hotel gropued for each aspect
'''
#Filtramos por item
reviews_of_item = [r for r in reviews if r.get('asin') == item_id ]
print('NÚMERO TOTAL DE REVIEWS:', len(reviews_of_item))
review_dfs = [get_review_resume(r.get('reviewText'), aspect_vocabulary) for r in reviews_of_item ]
#df contains all rows from all item reviews
df = pd.concat(review_dfs)
if(debug):#For testing purpose
print('------DEBUG----------------------------------------------------------')
print('reviews:', len(reviews_of_item),'\n :')
a = [print(r.get('reviewText'), '\n\n')for r in reviews_of_item]
print('all aspect concated for this hotel:')
print(df)
'''
print('POLARIDADES')
print('POSITIVAS\n',df_postive_polarity )
print()
print('NEGATIVAS\n',df_negative_polarity )
print()
print('NEUTRALES\n',df_neutral_polarity )
print()
'''
print('--------------------------------------------------------------------\n\n\n')
#Getint the top N metioned terms
terms = df['Word'].values.tolist()
most_commmon_terms = Counter(terms).most_common(n_most_common_words)
df = df[['Polarity', 'Aspect']]
#Counting Polatrity
df_postive_polarity =df.loc[df['Polarity'] > 0].groupby('Aspect').agg(['count'])
df_postive_polarity = df_postive_polarity.T.reset_index(drop=True).T
df_postive_polarity = df_postive_polarity.rename(columns={0: "Referencias_Positivas"})
df_negative_polarity =df.loc[df['Polarity'] < 0].groupby('Aspect').agg(['count'])
df_negative_polarity = df_negative_polarity.T.reset_index(drop=True).T
df_negative_polarity = df_negative_polarity.rename(columns={0: "Referencias_Negativas"})
df_neutral_polarity =df.loc[df['Polarity'] == 0].groupby('Aspect').agg(['count'])
df_neutral_polarity = df_neutral_polarity.T.reset_index(drop=True).T
df_neutral_polarity = df_neutral_polarity.rename(columns={0: "Referencias_Neutras"})
#Merging
df_resume = df.groupby('Aspect').agg(Polaridad_Media = ('Polarity','mean'),Total_Referencias = ('Polarity','count'))
df_resume = df_resume.merge(df_postive_polarity, on='Aspect', how='left')
df_resume = df_resume.merge(df_negative_polarity, on='Aspect', how='left')
df_resume = df_resume.merge(df_neutral_polarity, on='Aspect', how='left')
df_resume = df_resume.fillna(0)
df_resume = df_resume.astype({'Referencias_Positivas': 'int32'})
df_resume = df_resume.astype({'Referencias_Negativas': 'int32'})
df_resume = df_resume.astype({'Referencias_Neutras': 'int32'})
if(sort_by_ascending_plarity):
return df_resume.sort_values(by='Polaridad_Media', ascending=False), most_commmon_terms
else:
return df_resume, most_commmon_terms
Definimos también una función para visualizar el número de opiniones positivas/negativas/neutras como un gráfico de barras desglosado por aspectos. Dicho gráfico es interacctivo, permientiendo varios controles como ampliar la vista o tomar instantáneas:
import plotly.graph_objects as go
def plot_polarity_per_aspects(df_resumer_per_item):
'''Function to plot aspect_opinion_by_item() resume '''
df = df_resumer_per_item
x = df.index.to_list()
fig = go.Figure()
y1 = df['Referencias_Positivas'].to_list()
y2 = df['Referencias_Negativas'].to_list()
y3 = df['Referencias_Neutras'].to_list()
fig.add_trace(go.Bar(
x=x,
y=y1,
name='Nº Referencias Positivas',
marker_color='green'
))
fig.add_trace(go.Bar(
x=x,
y=y2,
name='Nº Referencias Negativas',
marker_color='red'
))
fig.add_trace(go.Bar(
x=x,
y=y3,
name='Nº Referencias Neutras',
marker_color='gray'
))
# Here we modify the tickangle of the xaxis, resulting in rotated labels.
fig.update_layout(barmode='group', xaxis_tickangle=-45)
fig.show()
Definimos una función para ver en un diagrama de sectores las palabras más mencionadas:
def plot_pie_chart(most_commmon_terms):
# This dataframe has 244 lines, but 4 distinct values for `day`
x = [x for (y,x) in most_commmon_terms ]
y = [y for (y,x) in most_commmon_terms ]
fig = px.pie( values=x, names=y, title='Most metioned terms')
fig.show()
Para mostrar el funcionamiento, escogemos un hotel con 5 reviews, las monstramos y mostramos el resumen que hacemos con ellas:
item = 'CYMG5AsrhkhUPro2c6NSUA'
resume_1, most_commmon_terms_1 = aspect_opinion_by_item(item, data_hotel,aspects_hotel_extended_wordnet,debug = True)
resume_1
plot_polarity_per_aspects(resume_1)
La imagen de arriba nos puede resultar útil para de un simple vistazo ver que las reseñas tiene una valoración positiva en cuanto a su precio o el bar, mientras que tiene una reseña negativo frente a algunos servicios. Si bien nos podemos hacer una idea general de las reseñas podemos ver que hay cierta controversia en respecto al servicio y al staff.
Vemos los términos más comunes mencionados:
plot_pie_chart(most_commmon_terms_1)
Vemos como room que se repite 2 veces aparece en el top 1, algo logico teniendo en cuenta que estamos viendo reseñas de hoteles.
Usando el mismo hotel que aparece en la review 0 que hemos estado usando como referencia, nos encontramos con 195 reviews, aquí sacamos el resumen de su polaridad:
Odenamos además los aspectos por polaridad positiva, para ver cuales son los puntos fuertes del hotel:
item = '8ZwO9VuLDWJOXmtAdc7LXQ'
resume_2, most_commmon_terms_2= aspect_opinion_by_item(item, data_hotel,aspects_hotel_extended_wordnet,debug = False, sort_by_ascending_plarity = True)
resume_2
Pintamos los resultados gráficamente, esto nos permite ver de un vistazo rápido una "valoración promedia" de los aspectos del hotel, siendo lo mejor valorado del hotel el edificio en si, la piscina, el bar o los baños.
plot_polarity_per_aspects(resume_2)
Pintamos los términos más mecionados(del top 5):
plot_pie_chart(most_commmon_terms_2)
Esto nos permite deducir que los topicos de las opciones del hotel hacen referencia a estos cunco aspectos, algo que encaja ya que la gran mayoria podían ser tags de busqueda de una web de hoteles.